# RulangBridge - ruby erlang bridge
# Copyright (C) 2007-2008 tosik
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.


#
#=Rulang Bridge - Ruby to Erlang Bridge
#Ruby から Erlang を扱えるようにする
#
#Authors::  tosik
#Version::  1.0.1.0 2007-05-24
#
require "socket"

module RulangBridge

  #
  # Erlang サーバの関数を Ruby のメソッドのように扱えるようにするクラス
  #
  class Rulang

    # 初期化する
    # [host]
    #   Erlang サーバのホスト名(デフォルトは"localhost")
    # [port]
    #   Erlang サーバのポート番号(デフォルトは"9900")
    # [async]
    #   非同期通信をするかどうか(デフォルトは"false")
    def initialize(mod, host="localhost", port=9900, async=false)
      @mod = mod
      @erlang = Erlang.new(host, port)
      @async = async
    end

    # Erlang の任意の関数を実行する
    # note: 定義されていないメソッドが呼び出されるとこのメソッドが実行される
    def method_missing(symbol, *args)

      # 非同期通信を行う場合、コールバックするブロックを取り出す
      block = args.pop if @async == true

      # Erlang 用のコマンドを作る
      command = @erlang.make_command(@mod, symbol, args)

      # Erlang に送信する
      begin
        if @async == true then
          # 非同期ならスレッドを生成してコールバックする
          Thread.new do
            res = @erlang.eval(command)
            raise res if @erlang.is_error(res)
            block.call(eval(res))
          end
        elsif @async == false then
          # 同期ならすぐに戻り値を返す
          res = @erlang.eval(command)
          raise res if @erlang.is_error(res)
          eval res
        end
      rescue => res
        # Erlang からエラーが返ってきたら例外を出す
        raise "[Error]=>#{res}"
      end
    end
  end

  #
  # Erlang を操作するクラス
  #
  class Erlang

    # Erlang インスタンスを生成する
    # [host]
    #   Erlang サーバのホスト名(デフォルト値は"localhost")
    # [port]
    #   Erlang サーバのポート番号(デフォルト値は"9900")
    def initialize(host="localhost", port=9900)
      @host, @port = host, port
    end

    # Erlang にコマンドを送信して、戻り値を文字列で返す
    # [command]
    #   送信するコマンド
    def eval(command)
      socket = TCPSocket.new(@host, @port)
      socket.write(command)
      socket.gets # ...?
    end

    # Erlang 用のコマンドを作成する
    # [erlang_mod]
    #   Erlang のモジュール
    # [elrang_func]
    #   Erlang の関数
    # [args]
    #   Erlang 用に変換したい Ruby の配列
    def make_command(erlang_mod, erlang_func, args)
      "#{erlang_mod}:#{erlang_func}(#{to_erlang_args(args)})."
    end

    # 引数を Erlang 用に変換する
    # [args]
    #   変換したい引数
    def to_erlang_args(args)
      args.map{ |x| to_erlang_literal x }.join(',')
    end

    # Ruby のオブジェクトを Erlang 用に変換する
    # [x]
    #   Ruby のオブジェクト
    def to_erlang_literal(x)
      if x.kind_of?(String) then
        "\"#{x}\""
      elsif x.kind_of?(Integer) then
        x
      elsif x.kind_of?(Float) then
        x
      elsif x.kind_of?(Array) then
        "[#{x.map{|y| to_erlang_literal(y)}.join(',')}]"
      else
        raise "Can't use \"#{x.type} Object\" in arguments."
      end
    end

    # Erlang の戻り値がエラーかどうか評価する
    # [str]
    #   評価したい文字列
    def is_error(str)
      str[0..5] == "Error:"
    end
  end
end

